package com.hqd.ch03.v44.core;

import com.hqd.ch03.utils.LinkedMultiValueMap;
import com.hqd.ch03.utils.MultiValueMap;
import com.hqd.ch03.utils.ReflectionUtils;

import java.util.*;

public class CollectionFactory {
    private static final Set<Class<?>> approximableCollectionTypes = new HashSet<>();

    private static final Set<Class<?>> approximableMapTypes = new HashSet<>();


    static {
        // Standard collection interfaces
        approximableCollectionTypes.add(Collection.class);
        approximableCollectionTypes.add(List.class);
        approximableCollectionTypes.add(Set.class);
        approximableCollectionTypes.add(SortedSet.class);
        approximableCollectionTypes.add(NavigableSet.class);
        approximableMapTypes.add(Map.class);
        approximableMapTypes.add(SortedMap.class);
        approximableMapTypes.add(NavigableMap.class);

        // Common concrete collection classes
        approximableCollectionTypes.add(ArrayList.class);
        approximableCollectionTypes.add(LinkedList.class);
        approximableCollectionTypes.add(HashSet.class);
        approximableCollectionTypes.add(LinkedHashSet.class);
        approximableCollectionTypes.add(TreeSet.class);
        approximableCollectionTypes.add(EnumSet.class);
        approximableMapTypes.add(HashMap.class);
        approximableMapTypes.add(LinkedHashMap.class);
        approximableMapTypes.add(TreeMap.class);
        approximableMapTypes.add(EnumMap.class);
    }


    private CollectionFactory() {
    }

    public static boolean isApproximableCollectionType(Class<?> collectionType) {
        return (collectionType != null && approximableCollectionTypes.contains(collectionType));
    }

    public static boolean isApproximableMapType(Class<?> mapType) {
        return (mapType != null && approximableMapTypes.contains(mapType));
    }

    public static <K, V> Map<K, V> createApproximateMap(Object map, int capacity) {
        if (map instanceof EnumMap) {
            EnumMap enumMap = new EnumMap((EnumMap) map);
            enumMap.clear();
            return enumMap;
        } else if (map instanceof SortedMap) {
            return new TreeMap<>(((SortedMap<K, V>) map).comparator());
        } else {
            return new LinkedHashMap<>(capacity);
        }
    }

    public static <E> Collection<E> createApproximateCollection(Object collection, int capacity) {
        if (collection instanceof LinkedList) {
            return new LinkedList<>();
        } else if (collection instanceof List) {
            return new ArrayList<>(capacity);
        } else if (collection instanceof EnumSet) {
            // Cast is necessary for compilation in Eclipse 4.4.1.
            Collection<E> enumSet = (Collection<E>) EnumSet.copyOf((EnumSet) collection);
            enumSet.clear();
            return enumSet;
        } else if (collection instanceof SortedSet) {
            return new TreeSet<>(((SortedSet<E>) collection).comparator());
        } else {
            return new LinkedHashSet<>(capacity);
        }
    }

    public static <E> Collection<E> createCollection(Class<?> collectionType, int capacity) {
        return createCollection(collectionType, null, capacity);
    }

    public static <K, V> Map<K, V> createMap(Class<?> mapType, Class<?> keyType, int capacity) {
        if (mapType.isInterface()) {
            if (Map.class == mapType) {
                return new LinkedHashMap<>(capacity);
            } else if (SortedMap.class == mapType || NavigableMap.class == mapType) {
                return new TreeMap<>();
            } else if (MultiValueMap.class == mapType) {
                return new LinkedMultiValueMap();
            } else {
                throw new IllegalArgumentException("Unsupported Map interface: " + mapType.getName());
            }
        } else if (EnumMap.class == mapType) {
            return new EnumMap(asEnumType(keyType));
        } else {
            if (!Map.class.isAssignableFrom(mapType)) {
                throw new IllegalArgumentException("Unsupported Map type: " + mapType.getName());
            }
            try {
                return (Map<K, V>) ReflectionUtils.accessibleConstructor(mapType).newInstance();
            } catch (Throwable ex) {
                throw new IllegalArgumentException("Could not instantiate Map type: " + mapType.getName(), ex);
            }
        }
    }

    public static <E> Collection<E> createCollection(Class<?> collectionType, Class<?> elementType, int capacity) {
        if (LinkedHashSet.class == collectionType || HashSet.class == collectionType ||
                Set.class == collectionType || Collection.class == collectionType) {
            return new LinkedHashSet<>(capacity);
        } else if (ArrayList.class == collectionType || List.class == collectionType) {
            return new ArrayList<>(capacity);
        } else if (LinkedList.class == collectionType) {
            return new LinkedList<>();
        } else if (TreeSet.class == collectionType || NavigableSet.class == collectionType
                || SortedSet.class == collectionType) {
            return new TreeSet<>();
        } else if (EnumSet.class.isAssignableFrom(collectionType)) {
            return (Collection<E>) EnumSet.noneOf(asEnumType(elementType));
        } else {
            if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
                throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
            }
            try {
                return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();
            } catch (Throwable ex) {
                throw new IllegalArgumentException(
                        "Could not instantiate Collection type: " + collectionType.getName(), ex);
            }
        }
    }

    private static Class<? extends Enum> asEnumType(Class<?> enumType) {
        if (!Enum.class.isAssignableFrom(enumType)) {
            throw new IllegalArgumentException("Supplied type is not an enum: " + enumType.getName());
        }
        return enumType.asSubclass(Enum.class);
    }
}
